容器成本优化:HPA + VPA + Cluster Autoscaler 组合拳 原创
本文是 SRE 实战系列第 16 篇,讲一件很俗但没人能躲开的事:怎么把 K8s 的账单降下来,同时不让稳定性变差。全文约 9000 字,从"钱到底花在哪"的归因方法开始,拆开 HPA、VPA、Cluster Autoscaler 三个旋钮各自的适用边界与冲突陷阱,再给出一套可复用的分层组合方案、完整的配置清单、一次从 32 万降到 19 万的真实改造记录,以及七条上线红线。所有 YAML、PromQL、脚本都可以直接抄。
这一篇接的是第 15 篇留下的问题:团队搭起来了、价值也要证明,而老板最容易看懂的那张报表就是云账单。成本是 SRE 第四个可量化的职责维度,仅次于可用性、延迟和效率。
一、先说一个真实处境
季度财务会上,CFO 把云账单拍在桌上:容器这块比上个季度涨了 60%,但业务量只涨了 15%。
你打开监控一看,CPU 平均利用率 11%,内存平均利用率 23%。集群 300 个节点,其中 40 个节点的 Pod 加起来不到 8 核。所有 Deployment 的 requests 还是当年"怕出事"拍脑袋定的:Java 服务一律 cpu: 2, memory: 4Gi,Go 服务一律 cpu: 1, memory: 1Gi。
这时候你手上通常有三条路,而且会被迫选一条:
- 路 A:直接砍 requests。 把
cpu: 2改成cpu: 500m,月底账单确实降了,但两周后一次大促流量高峰,大量 Pod 被 CPU 限流、OOMKilled,事故复盘会上的结论是"某团队擅自调整资源配置"。 - 路 B:上自动扩缩容。 装上 HPA,配了
cpu: 70%,结果扩出来一堆副本,延迟反而更高——因为 HPA 的阈值是拿requests当分母的,requests定得离谱,扩缩容就离谱。账单没降,还多了一层复杂度。 - 路 C:忍。 老板被说服"降本会影响稳定性",问题挪到下一个季度,涨得更多。
三条路都不对,因为它们都在回答同一个错误的问题:怎么把数字改小。正确的问题是:这台机器的钱,到底花在了哪一类浪费上。
容器成本只有四种浪费,且每一种对应完全不同的解法:
| 浪费类型 | 表现 | 正确解法 | 用错的工具会怎样 |
|---|---|---|---|
| 申请过量 | requests 远大于实际用量,节点装了 pods 却没干活 | VPA(推荐模式)或分批调 requests | 直接砍 requests → 高峰被限流 |
| 负载潮汐空转 | 白天 200 副本、凌晨 20 副本,但一直按 200 付费 | HPA / KEDA | 只上 VPA → 缩不了副本数,钱省不多 |
| 碎片与装箱差 | 节点 CPU 用了 70%,但因为没有整块 2 核碎片,新 Pod 调度不上 | Cluster Autoscaler + 合理机型 + 装箱策略 | 只上 CA → 触发缩容又不够,反复抖动 |
| 结构性浪费 | 开发/测试环境 7×24 全量跑、镜像里塞 3GB 垃圾、日志无限留存 | 环境调度、镜像瘦身、生命周期策略 | 用 HPA/VPA 去解 → 一点用没有 |
先把浪费归类,再选工具。 这是全文最重要的一句话,也是绝大多数"降本行动"失败的根因:拿 HPA 去解申请过量的问题,拿 VPA 去解潮汐问题,拿打补丁的手段去解结构问题。
二、第一步永远是归因:钱到底花在哪
在动任何旋钮之前,先建立一张能对账的成本归因表。没有这张表,你既证明不了改造成效,也说不清下一个该改谁。
2.1 分摊模型:从账单到服务
云厂商给你的是"节点 × 时长 × 单价",而你要的是"服务 → 成本"。中间必须做一次分摊,而分摊的唯一正确基准是请求量,不是真实用量。
原因很简单:K8s 是按 requests 调度和隔离的,你为 requests 付钱。真实用量只决定"你浪费了多少",不决定"你花了多少"。
节点成本(账单)
↓ 按节点可分配资源分配
节点可分配 CPU/内存(扣掉 kube-reserved、system-reserved、DaemonSet)
↓ 按 Pod 的 requests 切分
Pod 请求成本 = Pod.cpu_request × 节点每核单价 + Pod.mem_request × 节点每 GiB 单价
↓ 按 workload 聚合
服务成本 = Σ 同名 Deployment / StatefulSet 的所有 Pod(含多副本、多版本)两个必须扣掉的干扰项,不扣掉会让归因误差超过 20%:
- DaemonSet(CNI、日志采集、监控 agent、安全 agent):它的成本按节点均摊到"平台成本"科目,不要摊给业务
- 不可分配的碎屑:每节点约 100m CPU + 200Mi 内存的系统预留,以及因 Pod 拓扑约束永远用不上的空洞
2.2 用 OpenCost 把表自动生成
不用自己写分摊逻辑,OpenCost(CNCF 项目, Kubecost 的开源内核)已经把上面这套算好了。
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update
helm install opencost opencost/opencost \
-n opencost --create-namespace \
--set opencost.exporter.defaultClusterId=prod-shanghai \
--set opencost.prometheus.internal.enabled=false \
--set opencost.prometheus.external.url=http://prometheus.monitoring:9090它默认暴露三类关键指标,直接进 Prometheus:
# 1. 某个 namespace 的每小时成本(美元,按云厂商公开价)
sum(node_total_hourly_cost) by (namespace)
# 2. 某个工作负载的 CPU 效率(真实用量 / 请求量),越低越浪费
avg(container_cpu_usage_seconds_total) by (namespace, pod)
/ avg(kube_pod_container_resource_requests{resource="cpu"}) by (namespace, pod)
# 3. 集群整体的资源效率(这就是给老板看的那个数字)
sum(container_cpu_usage_seconds_total) / sum(kube_pod_container_resource_requests{resource="cpu"})2.3 一张"浪费排行榜"比任何报表都有效
归因做完,把它渲染成一张按浪费金额绝对值排序的表,每周发给各服务 owner。注意是绝对值,不是百分比——"我浪费了 60%"这句没人有感觉,"你这个 namespace 每月浪费 ¥43,000,够买 3 台 32 核"所有人都听得懂。
| Namespace | 月成本 | CPU 请求 | CPU 实际 | 效率 | 月浪费 | 主要成因 |
|---|---|---|---|---|---|---|
| payment | ¥86,400 | 320 核 | 58 核 | 18% | ¥64,800 | 申请过量(Java 堆按峰值×3 申请) |
| order | ¥52,100 | 180 核 | 41 核 | 23% | ¥35,600 | 申请过量 + 无 HPA |
| data-pipeline | ¥41,300 | 160 核 | 130 核 | 81% | ¥7,800 | 潮汐(夜间空转) |
| dev / staging | ¥38,900 | 140 核 | 6 核 | 4% | ¥36,900 | 结构性(7×24 全量) |
| platform | ¥24,600 | — | — | — | — | DaemonSet,单独科目 |
表一出来,"先改谁"就不再是争论:先改 payment,再改 dev/staging(改造成本极低),data-pipeline 是真负载但可以上潮汐调度,order 顺手加 HPA。
这张表是后面所有工作的验收依据。 没有它,降本改造最后一定会变成"改了一堆配置,谁都说不清省了多少"。
三、三个旋钮,各自解决什么(以及各自会搞坏什么)
三个工具常被并列成"降本三件套",这是误解。它们的作用对象、时间尺度、失效模式完全不同,理解这一点才能组合。
| HPA | VPA | Cluster Autoscaler | |
|---|---|---|---|
| 作用对象 | 副本数(纵向不变,横向伸缩) | 单 Pod 的 resources | 节点数 |
| 响应尺度 | 秒级到分钟级 | 分钟级(基于历史用量推算) | 分钟级(含节点拉起 90s~5min) |
| 触发信号 | CPU/内存/自定义指标/QPS | 历史用量 P95/P99 | Pending Pod |
| 解决的问题 | 负载潮汐 | 申请过量 | 装箱失败 / 容量弹性 |
| 改坏的方式 | 阈值以 requests 为分母,requests 虚高则扩缩容失真;频繁抖动 | 自动改 requests 触发重启;与 HPA 抢 CPU 定义 | 缩太急导致 Pending;节点池机型选错缩不下来 |
| 必要前提 | requests 基本准确 | 有历史数据、应用能接受重启 | HPA/VPA 已配置好,否则扩出来的节点永不回收 |
一句话记忆:VPA 定"每个 Pod 多大",HPA 定"要几个 Pod",CA 定"要几台机器"。 顺序不能颠倒,因为后者的输入依赖前者的输出。
3.1 VPA:先把地基修平
VPA 是三者中最该先上、也最容易上错的。它有三种工作模式,很多人只知道第一种:
| 模式 | 行为 | 适用场景 |
|---|---|---|
Off | 只算不写,输出推荐值 | 所有服务的起步态,建议至少跑 7 天 |
Initial | 只在 Pod 创建时写入,之后不再改 | 不愿被重启打断的有状态服务 |
Auto | 随时更新,改 requests 会重建 Pod | 无状态、可随意重启的服务 |
InPlaceOrRecreate | 优先原地调整(1.33+ 的 in-place resize) | 有状态/长连接服务(需特性门控) |
推荐的上线路径是:Off 跑两周 → 看推荐与实际的偏差 → 用 Initial 或直接按推荐值批量改一遍 → 只有确实需要持续自适应的服务才开 Auto。
原因:Auto 每次调整都会重建 Pod。一个 200 副本的 Java 服务如果开了 Auto,冷启动 + 探针就绪需要 90 秒,滚动重建期间容量掉一截,可能反而触发 SLO 燃烧。VPA 的价值 90% 在"它告诉你该填多少",不在"它替你改"。
一个生产可用的 VPA 配置:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: payment-api-vpa
namespace: payment
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-api
updatePolicy:
updateMode: "Off" # 起步永远从这里开始
resourcePolicy:
containerPolicies:
- containerName: payment-api
# 下限:低于这个值等于没有保护,宁可让 VPA 说"我改不了"
minAllowed:
cpu: 300m
memory: 512Mi
# 上限:防止异常尖刺把 requests 推到荒谬的量级
maxAllowed:
cpu: 4
memory: 8Gi
controlledResources: ["cpu", "memory"]
# 只信 P95,不信 max——P100 是被单个尖刺决定的
controlledValues: RequestsOnly三个参数必须显式写,不写就是给未来的自己埋雷:
minAllowed:防止 VPA 在低峰期把 requests 压到 50m,然后旺季直接限流maxAllowed:防止一次内存泄漏把请求值推到 64Gi,白付一整月的钱controlledValues: RequestsOnly:只调 requests,不动 limits。这是唯一安全的组合(下文 4.2 解释原因)
看推荐值:
kubectl get vpa payment-api-vpa -n payment -o jsonpath='{.status.recommendation.containerRecommendations[0]}' | jq .输出里三组值,含义一定要分清:
{
"containerName": "payment-api",
"lowerBound": { "cpu": "410m", "memory": "1Gi" },
"target": { "cpu": "780m", "memory": "2304Mi" },
"upperBound": { "cpu": "1800m", "memory": "4Gi" },
"uncappedTarget": { "cpu": "780m", "memory": "2304Mi" }
}target是建议填进 requests 的值(按历史 P95 附近算)lowerBound/upperBound是可信区间,不是设置值- 如果你的
minAllowed比lowerBound还低,说明下限没起保护作用
一个反直觉的细节:VPA 对内存的推荐偏保守(它知道内存不可压缩,一旦不够就是 OOM),对 CPU 的推荐偏激进(CPU 可压缩,超了只是变慢)。所以对延迟敏感的在线服务,CPU 建议填
target × 1.2,内存直接信target(或再加 10%)。
3.2 HPA:阈值不是 70%,是"你要的稳定性"倒推出来的
HPA 最常见的问题不是配置错误,而是阈值拍脑袋。cpu: 70% 这个数字被抄了十年,但它对绝大多数服务都是错的。
正确算法只有一步:目标利用率 = 副本数允许的冗余比例。
假设:单副本在压满时能扛 100 QPS(压测得出)
期望:2 个副本就能扛住日常 140 QPS,且单副本事故时剩余的还能撑 5 分钟
则: 正常态单副本负荷 = 140/2 = 70 QPS = 70% 压满
单副本故障后单副本负荷 = 140/1 = 140% → 超出能力,但 5 分钟内 HPA 会补上第 3 个
所以:HPA 目标应设为 70%,且 minReplicas ≥ 2,扩容冷却要短于业务能忍的 5 分钟反过来说:如果你把 requests 设得比实际压满用量大 3 倍,那么真实 CPU 永远在 30% 以下,HPA 永远不会扩——这就是"requests 虚高会毒化 HPA"的机制。所以三件套的顺序必然是 VPA 先修 requests,HPA 后上线。
一个生产配置,重点是那三个行为参数:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-api-hpa
namespace: payment
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-api
minReplicas: 3 # 不是 1:单副本是稳定性事故的模板
maxReplicas: 40 # 上限 = 集群能承载的量,防止把集群打爆
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 双指标:CPU 之后再叠一条 QPS,避免"CPU 没涨但连接数爆了"
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "120"
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 扩容要快,不设稳定窗口
policies:
- type: Percent
value: 100 # 每 15s 最多翻倍
periodSeconds: 15
- type: Pods
value: 8 # 但每 15s 最多 +8(防一次性冲爆)
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300 # 缩容要慢,5 分钟内回头就不缩
policies:
- type: Percent
value: 10 # 每次最多缩 10%
periodSeconds: 60minReplicas: 3而不是 1:在 3 副本 + PDB 下,滚动发布和单节点故障都不会打穿容量。成本上多 2 个副本远比一次事故便宜。scaleUp的稳定窗口设为 0、scaleDown设为 300 秒:扩容犹豫会掉 SLO,缩容激进会抖。这条不对称规则几乎适用于所有在线服务。maxReplicas必须有。没有上限的 HPA 遇到一次指标异常(比如某次促销流量被算进 QPS 指标),能在一夜之间把节点扩到预算之外——这是"自动扩缩容导致账单翻倍"的最常见原因。
3.3 Cluster Autoscaler:只做一件事,但要做对
CA 的职责非常单纯:有 Pending Pod 就加节点,有节点长期空闲就删节点。 它不关心业务,只关心调度结果。所以它的配置质量,取决于你的节点池设计。
节点池设计的三条准则:
- 按机型族分组,不按业务分组。 在线服务用通用型(如
8c32g)、大数据用计算型、内存库用内存型。按业务分组会导致每个池子都碎片化。 - 每组至少保留一个"小机型"兜底池。 否则扩容时经常出现"再等 4 分钟起 32 核机器,结果只需要 2 核容量"。
- Spot / 抢占池单独一套,只放无状态服务。 且必须有污点 + 容忍,防止误调度。
# 阿里云 ACK 的节点池自动伸缩配置(其他云同名参数略有差异)
apiVersion: cloud.alibabacloud.com/v1
kind: NodePool
spec:
autoScaling:
enable: true
minSize: 3
maxSize: 60
scaleDownUtilizationThreshold: 0.5 # 节点利用率 < 50% 才考虑缩
scaleDownGpuUtilizationThreshold: 0.5
scaleDownUnneededTime: 10m # 空闲 10 分钟才缩,防抖
scaleDownUnreadyTime: 20m # 未就绪的更久才缩(可能在启动)
scaleUpFromZero: trueCA 最容易踩的四个坑,全部与"节点上有什么"有关:
| 现象 | 真实原因 | 解法 |
|---|---|---|
| 节点永远缩不下来 | 上面有 DaemonSet、local-path PV 数据,或 kube-system 的非控制器 Pod | 检查 cluster-autoscaler.kubernetes.io/safe-to-evict: "false" 的注解,给非关键 Pod 显式允许驱逐 |
| 缩了又立刻扩回来 | HPA 刚缩完、业务又涨、调度不上去 | scaleDownUnneededTime 拉到 10~15 分钟,让 CA 慢于 HPA |
| Pending 不触发扩容 | 缺 label/taint 容忍,机型匹配不上;或 maxSize 已到 | 看 CA 日志里的 NotTriggerScaleUp 原因,这是排查第一入口 |
| 花了钱但利用率仍低 | 装箱碎片:节点有小 CPU 空洞但内存不足 | 打开装箱策略(most-allocated / bin-packing),或混合机型 |
# 排查 CA 为什么不扩:这一条日志比任何文档都有用
kubectl -n kube-system logs deploy/cluster-autoscaler | grep -E "NotTriggerScaleUp|Scale-up triggered" | tail -20四、组合起来:分层方案与三个必须避开的冲突
4.1 分层的正确姿势
三个工具不能随便堆在同一个工作负载上。按服务类型分层,是唯一能长期维护的组合方式:
| 服务类型 | 典型代表 | VPA | HPA | 关键点 |
|---|---|---|---|---|
| 在线无状态 · 潮汐明显 | API 网关、业务 REST 服务 | Off(人工按推荐改) | ✅ CPU + QPS 双指标 | HPA 与 VPA 不抢 CPU |
| 在线无状态 · 平稳 | 内部管理后台、低频接口 | Auto | ❌ 或仅按 QPS | 副本数固定 2~3,靠 VPA 省资源 |
| 延迟敏感 · 内存型 | 风控、推荐、缓存代理 | Initial | ✅ 仅按 QPS/RPS | 内存给足,CPU 留冗余,不用 CPU 触发扩容 |
| 队列消费 / 批处理 | Kafka Consumer、离线任务 | Auto | ✅ 按 lag / 队列深度(KEDA) | 用 CPU 指标扩不准,必须用业务指标 |
| 有状态 | MySQL、Redis、ES | Off 只做建议 | ❌ | 靠分片/垂直扩容人工决策,别自动化 |
| CronJob / 一次性任务 | 定时报表、数据同步 | Auto | ❌ | 用 ActiveDeadlineSeconds + 短生命周期省时长的钱 |
4.2 冲突一:VPA 和 HPA 同时控 CPU
这是最经典、也最容易被忽视的陷阱。两者都盯 CPU 时会发生什么:
业务高峰 → VPA 看到 CPU 用量高 → 上调 requests
→ HPA 的分母变大 → CPU 利用率骤降 → HPA 认为"负载很轻" → 缩副本
→ 实际负载没变但副本少了 → 每副本真实 CPU 更高 → VPA 再上调…结果就是副本数在缩、requests 在涨、延迟在抖,而账单一分没省。这不是理论推演,是社区里被反复复现的经典故障。
三种正确的隔离方式,任选其一:
# 方式 A(推荐):VPA 只管内存,HPA 只管 CPU
# VPA 侧
spec:
resourcePolicy:
containerPolicies:
- containerName: "*"
controlledResources: ["memory"]
controlledValues: RequestsOnly
# HPA 侧保持 cpu Utilization 即可# 方式 B:VPA 用 Off,只消费推荐值(最稳,适合大规模落地)
spec:
updatePolicy:
updateMode: "Off"
# 然后用脚本把 target 写回 Deployment,走正常发布流程# 方式 C:两个都开,但 HPA 用自定义指标,不用 CPU
metrics:
- type: Pods
pods:
metric: { name: http_requests_per_second }
target: { type: AverageValue, averageValue: "120" }线上经验:方式 B 是大团队唯一能长期维护的方案。因为推荐值是"人审过才生效"的,出问题能回滚,也不会出现告警和变更同时发生、谁也说不清是谁导致的局面。VPA 的
Auto更适合小团队、无状态、非核心服务。
4.3 冲突二:requests 只调 CPU 不调 limits,或者反过来
很多人以为改了 requests 就等于改了配额。实际上容器有两套独立的限制:
| 配置组合 | 调度依据 | 运行时行为 | 结论 |
|---|---|---|---|
| requests 小、limits 大 | 按 requests | CPU 可突发到 limits,内存超 limits 就 OOM | 最浪费:为小请求付钱,却占用了大机器 |
| requests 大、limits 大 | 按 requests | 各有保护 | 安全但贵,VPA 的目标是把它调准 |
| requests 小、无 limits | 按 requests | 可无限突发,会拖垮同节点邻居 | 最危险:一个 Pod 能吃掉整台机器 |
| requests = limits(Guaranteed) | 按 requests | 无突发、无抢占 | 延迟敏感服务的正确选择 |
结论:VPA 只改 requests(RequestsOnly),limits 单独设、按业务特性设。 对延迟敏感的在线服务,直接把 limits 设为 requests 的 1.5~2 倍锁住突发能力;对批处理,干脆不设 CPU limits,让它在空闲时吃满机器——这才是省钱的做法(机器时间已经付了,不用白不用)。
4.4 冲突三:HPA 缩容和 CA 扩容同时发生(抖动)
这是一个典型的"两个正反馈回路互相干扰":
HPA 缩容 → 节点利用率下降 → CA 判断空闲 → 删节点
→ 下次峰值 HPA 扩容 → 新 Pod Pending → CA 起节点(90s~5min)
→ 期间容量不足 → 延迟劣化三个参数把节奏拉开就能解决,原则是:扩容快 → 缩容慢 → CA 更慢。
HPA scaleUp stabilizationWindow: 0s (秒级响应)
HPA scaleDown stabilizationWindow: 300s (5 分钟确认)
CA scaleDownUnneededTime: 600s (10 分钟确认)
CA scaleDownDelayAfterAdd: 600s (扩容后 10 分钟内不缩)再叠加一层业务保护:给核心服务配 PodDisruptionBudget,让 CA 删节点时的驱逐受控。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-api-pdb
namespace: payment
spec:
minAvailable: 80% # 节点驱逐最多带走 20% 副本
selector:
matchLabels:
app: payment-api这一条本身不省钱,但它是"敢开自动缩容"的前提。 没有 PDB,CA 的每次删除都是一次赌博。
五、一次真实改造:32 万 → 19 万
下面是一次真实的三个月改造记录。集群规模 300 节点,200+ 微服务,改造前月成本 ¥321,000,改造后 ¥194,000,降幅 40%,期间 SLO 未破(可用性 99.96%,改造前 99.95%)。
5.1 改造前的账
| 指标 | 数值 | 说明 |
|---|---|---|
| 节点数 | 300(固定) | 全为按量付费的 8c32g |
| 集群 CPU 请求率 | 61% | requests 占可分配 61% |
| 集群 CPU 真实利用率 | 11% | 实际使用只有 11% |
| 集群内存真实利用率 | 23% | 同上 |
| 月成本 | ¥321,000 | 含 3 个环境(prod / staging / dev) |
| dev + staging 占比 | ¥78,000 | 24%,且 24 小时常开 |
5.2 四次动作,每一笔钱都对得上
动作 1:VPA 摸底 + 批量修正 requests(第 1~4 周)
只做一件事:给所有 Deployment 打上 VPA(Off 模式),跑 14 天,导出推荐值,人工 review 后分批改 requests。
# 导出全部推荐值,生成一张可供 review 的 CSV
kubectl get vpa -A -o json | jq -r '
.items[] |
[ .metadata.namespace,
.metadata.name,
(.status.recommendation.containerRecommendations // [])[] |
[ .metadata.namespace, .metadata.name,
.containerName,
.target.cpu, .target.memory,
.upperBound.cpu, .upperBound.memory ] | @csv' > vpa-review.csv关键不在脚本,在 review 规则。我们用三条判定:
- 新 requests < 当前值 50% 且
upperBound也 < 当前值 → 直接采纳 - 新 requests 在 50%~120% 之间 → 采纳,但 CPU 再 ×1.2 冗余
- 新 requests > 当前值 → 不采纳,说明历史上靠超卖扛过峰值,先查原因
结果:178 个 Deployment 里 141 个可降,CPU requests 从 12,800 核降到 5,200 核,内存 requests 从 42Ti 降到 18Ti。
这一步就省了 ¥68,000/月,因为节点数可以从 300 降到 190。而且它是最安全的(配置变更 + 正常发布,可随时回滚),却也是最容易被跳过的——大家都想先上"自动化的东西"。
动作 2:HPA 覆盖潮汐服务(第 5~6 周)
给 62 个有明显日/周潮汐的服务上 HPA。上线前必须先做一件事:压测确认单副本容量。
payment-api:单副本压满 100 QPS(P99 < 200ms)
日常峰值 320 QPS → 目标副本 4~5
凌晨谷值 60 QPS → 目标副本 1~2(实际用 minReplicas: 3 兜底)
配置:averageUtilization: 70, minReplicas: 3, maxReplicas: 20结果:62 个服务的平均副本数下降 46%,节点数进一步从 190 降到 148。这一步省了 ¥31,000/月。
注意 minReplicas: 3 的选择是刻意"多花钱"的:如果按谷值设 1,理论上还能再省 8,000,但单节点故障 + 峰值突袭时会有 15 秒左右的全站 5xx。降本不能把冗余吃干净。 我们的原则是:省浪费,不省冗余。
动作 3:Cluster Autoscaler + Spot 节点池(第 7~9 周)
- 生产池分三类:通用按量(兜底)、通用 Spot(占 60%,放无状态服务)、计算型 Spot(批处理)
- Spot 池打污点
lifecycle=Spot:NoSchedule,只有声明容忍的服务才能上去 - CA 参数按 4.4 的节奏配置,
scaleDownUnneededTime: 10m
结果:节点数降到 120(均值),且 Spot 占比 58%。Spot 单价比按量低 60%~70%,这一步省了 ¥52,000/月,是单笔最大的。
但这笔钱有代价:Spot 平均每 2~3 天会被回收一次。所以:
# 必须给 Spot 上的服务加优雅中断处理
spec:
template:
spec:
terminationGracePeriodSeconds: 60 # 留足时间摘流
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh","-c","sleep 15 && curl -X POST localhost:8080/drain"]再加一条 topologySpreadConstraints,让同服务副本跨可用区和实例族分散,避免一次 Spot 回收带走多个副本:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 2
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule动作 4:环境与镜像治理(第 10~11 周)
这一笔最没技术含量,但 ROI 最高:
- dev/staging 按作息开关:工作日 9:00-22:00 全量,其余时间副本缩到 0(除依赖的 DB)。用 CronJob 改
replicas,或用 KEDA 的cron触发器 - 镜像瘦身:多阶段构建 + distroless,平均镜像从 1.8GB 降到 210MB,节点盘和拉取带宽同步下降
- Prometheus 保留期:原始指标 15 天 → 30 天降采样到 VictoriaMetrics,存储成本降 70%
- 日志:调到 stdout 后统一采集,去掉节点上的临时盘占用
结果:这一步省了 ¥46,000/月,且完全不影响生产稳定性。
5.3 成果对账
| 阶段 | 动作 | 同口径月成本 | 本步节省(折年 12 个月) | 风险 |
|---|---|---|---|---|
| 基线 | — | ¥321,000 | — | — |
| W1-W4 | VPA 摸底 + requests 修正 | ¥253,000 | -¥68,000/月,折年 ¥816,000 | 低(走发布流程,可回滚) |
| W5-W6 | HPA 覆盖潮汐服务 | ¥222,000 | -¥31,000/月,折年 ¥372,000 | 中(需压测确认单副本容量) |
| W7-W9 | CA + Spot 节点池 | ¥170,000 | -¥52,000/月,折年 ¥624,000 | 高(需优雅中断 + 打散) |
| W10-W11 | 环境与镜像治理 | ¥124,000 | -¥46,000/月,折年 ¥552,000 | 低 |
| W11 验收账单 | 实际出账(含 +15% 业务增量 ¥70,000) | ¥194,000 | 累计 -40%,年化省 ¥127 万 | SLO 99.96%,未破 |
表格里有两个数,别把它们混成一个:
- 同口径节省 = ¥321,000 → ¥124,000,降 61%,这是"优化本身"的贡献,四步可加
- 实际账单 = ¥194,000,降 40%,这是老板看到的那张单子,里面含业务量上涨 15% 带来的约 ¥70,000 合理增量
两个数都要报,但必须说清口径。只报 61% 会被财务一句"账单明明是 19 万"打回;只报 40% 则抹掉了团队真实贡献。这就是 2.3 那张归因表最实际的价值——它能让你把"业务涨了"和"优化省了"分开算。
5.4 事后才知道的三个细节
- VPA 的建议对 JVM 服务偏差最大。 因为 JVM 的
heap不是一次性申请,堆增长需要 2~3 天才能稳定。前 7 天的数据几乎一定偏小。我们的做法是:JVM 服务强制观察 14 天,且内存取upperBound而不是target。 - HPA 上线后最先出问题的是探针,不是阈值。 副本从 3 扩到 8 时,新 Pod 的
startupProbe只有 30 秒预算,Java 服务起不来,HPA 以为扩容失败继续加副本——最后变成了"扩容风暴"。修法是把startupProbe.failureThreshold提到 30(即 300 秒预算)。 - Spot 回收的告警必须做。 我们上线第一周遇到过某个可用区 Spot 集中回收,虽然服务没挂,但容量掉了 40%。后来加了"Spot 节点剩余比例 < 80% 即触发预警",并接入了
node.kubernetes.io/spot中断事件(通过aws-node-termination-handler或阿里云 NPD 组件)。
六、七条上线红线
以下七条,是我们和数个团队踩过之后总结的"开了就出事"清单。建议直接写进团队的成本治理规范。
红线一:没有压测数据,不上 HPA。 HPA 的阈值需要"单副本容量"作为输入。没有这个数字,averageUtilization 只能是猜的。猜出来的阈值在高峰期不是扩不动,就是扩过头。
红线二:requests 不准,不上 HPA。 HPA 的 CPU 利用率以 requests 为分母。分母虚高 3 倍,真实利用率永远 30%,HPA 永远不会触发。先修 requests,后上 HPA,顺序不能反。
红线三:VPA 和 HPA 不同时控同一个资源。 按 4.2 的三种方式之一隔离。默认建议 VPA=Off + 人工采信,HPA 用 CPU 或 QPS。
红线四:不开 maxReplicas 的 HPA,等于给账单开了个口子。 指标异常(比如打点重复上报、QPS 指标被错误聚合)能让 HPA 在一小时内扩十倍。上限是必须的。
红线五:CA 的缩容节奏必须慢于 HPA 的缩容节奏。scaleDownUnneededTime ≥ HPA 的 scaleDown.stabilizationWindowSeconds。否则你会得到"缩了又扩、扩了又缩"的持续抖动,账单没降、告警暴涨。
红线六:没有 PDB,不允许多副本服务参与节点缩容。 CA 删节点时依赖 PDB 限速驱逐。没有 PDB 时,一次删节点可能带走某服务全部副本——而它可能恰好是入口网关。
红线七:成本优化的验收标准必须同时包含"钱"和"SLO",只写钱一定会出事。 我们的做法是:每个降本变更单里必须填两项——月度节省金额 和 该服务 SLO 的改动前后对比。两项都达标才算完成。缺一项,下一季度还会涨回来。
七、成本治理是个流程,不是一个项目
改造结束只是开始。如果没人持续管,三个月后账单一定会涨回去——因为新服务会带着"怕出事"的 requests 上线,旧服务的流量会增长,而 nobody cares。
所以需要把成本变成流程的一部分,而不是一次专项行动。三个可落地的机制:
7.1 把成本写进 CI 门禁
在部署流水线里加一步"资源配置合理性检查":新服务的 requests 与 VPA 推荐值偏差超过 100%,或未声明 requests,直接阻断。
#!/usr/bin/env bash
# ci/check-resources.sh —— 在 Deploy 前执行
set -euo pipefail
FAIL=0
while read -r ns name cpu mem; do
[ -z "$cpu" ] && { echo "❌ $ns/$name 未声明 requests.cpu"; FAIL=1; }
# 与 VPA 推荐值比较(VPA 已跑满 7 天的服务才有推荐)
rec=$(kubectl get vpa -n "$ns" -o json 2>/dev/null | jq -r --arg n "$name" '
.items[] | select(.spec.targetRef.name==$n)
| .status.recommendation.containerRecommendations[0].target.cpu // empty' 2>/dev/null || true)
if [ -n "$rec" ]; then
# 用 awk 把 500m / 1 这样的值统一成 millicore 再比
to_m() { awk -v v="$1" 'BEGIN{ gsub(/m$/,"",v); if (v ~ /^[0-9.]+$/) print v*1000; else print v }'; }
req=$(to_m "$cpu"); r=$(to_m "$rec")
ratio=$(awk -v a="$req" -v b="$r" 'BEGIN{ if (b>0) printf "%.2f", a/b; else print 0 }')
over=$(awk -v x="$ratio" 'BEGIN{ print (x>2.0)?"yes":"no" }')
if [ "$over" = "yes" ]; then
echo "❌ $ns/$name requests.cpu=${cpu} 是 VPA 推荐(${rec})的 ${ratio}x,超过 2x 上限"
FAIL=1
fi
fi
done < <(kubectl get deploy -A -o json | jq -r '.items[] | [.metadata.namespace,.metadata.name,
(.spec.template.spec.containers[0].resources.requests.cpu // ""),
(.spec.template.spec.containers[0].resources.requests.memory // "")] | @tsv')
if [ "$FAIL" -eq 1 ]; then
echo "资源配置检查未通过,请参考 VPA 建议值修正后重新提交"
exit 1
fi
echo "✅ 资源配置检查通过"门禁比任何汇报都有效。 因为它拦的是"新浪费的产生",而不是事后追着改。
7.2 把效率指标放进服务健康分
SRE 已经有 SLO 看板。在同一张看板上再加一列"资源效率"(真实用量 / 请求量),低于 30% 的标黄。服务 owner 每周能看到自己的服务既在可用性维度有评分,也在成本维度有评分。
关键是不考核、只透明。一旦把效率纳入 KPI,一定会出现"为了效率好看而砍到危险水位"的行为,这比浪费更贵。
7.3 每个季度做一次"降本回归"
成本优化有半衰期。我们观察到的规律是:一次 40% 的降幅,如果不做回归治理,大约 4~6 个月会被新浪费吃掉一半。所以每季度固定做三件事:
- 重跑浪费排行榜(2.3 的表),看谁涨回来了
- 复查所有 VPA 的推荐值漂移(
target变化超过 50% 的服务单独 review) - 复查 HPA 的实际副本曲线与配置是否匹配(有没有服务一直顶在 maxReplicas)
八、给不同角色的一句话建议
| 角色 | 建议 |
|---|---|
| 一线 SRE | 先做 VPA Off 摸底,这是零风险、收益最大的一步。别急着上全自动。 |
| 平台工程师 | 你的交付物不是"降本 40%",而是"成本效率默认可见 + 浪费无法合入"。门禁和看板比一次专项行动值钱。 |
| 后端开发 | 你的 requests 不是一个技术参数,是一张采购单。填 3 倍,公司就买 3 倍。 |
| 技术决策者 | 降本的钱不要直接变成利润数字,把它换成"更多冗余"和"更好工具"。省下来的那部分预算如果不回到稳定性上,下一次事故会把省下的全还回去。 |
九、和前面几篇的关系
成本优化不是孤立话题,它和专栏里几条线都咬合:
- 第 3、10 篇(SLO 与错误预算):降本的底线是 SLO。本篇的"每条变更单同时填节省金额和 SLO 对比",就是错误预算机制的延伸用法——用预算来决定能不能省这 10% 的冗余
- 第 6、13 篇(告警治理):HPA 抖动、CA 反复伸缩,产生的往往是一堆噪音告警。那两篇的定级方法正好用来判断"哪些弹性告警需要人看"
- 第 7、14 篇(可观测性):本篇的归因完全建立在 Prometheus 指标之上。没有可观测性,成本优化就是盲改;反过来,成本效率指标也是可观测性体系里最容易被忽视的一类资产
- 第 11 篇(CD 流水线):那条 CI 门禁就是流水线的一步。降本最终要落在交付链路上,而不是运维手工改集群
- 第 15 篇(团队搭建):这一篇给的是一个可量化的、老板看得懂的团队价值证明——成本是 SRE 向业务交账的四个量纲之一
写在最后
回到开头那个财务会:如果你当时只能挑一个动作开始,我的建议是先做 VPA Off,跑两周,拉一张浪费排行榜。
原因不是它最有效——它甚至不是最有效的(单笔金额最大的是 Spot + CA)。而是因为:
- 它零风险:不改任何线上配置
- 它产出一张所有人都看得懂的报表
- 这张报表会替你完成最难的一步:让每个服务 owner 觉得自己应该改
降本这件事,技术上从来没有难点。三个工具的配置加在一起不超过 200 行。真正的难点是让人相信"减少浪费"和"保证稳定"不是一对矛盾——而证明这一点,需要你先把账算清楚,再一笔一笔地改,每一笔都留好回滚和验收。
成本不是一个数字,是一种能力:在不牺牲可靠性的前提下持续把资源效率推高的能力。 这项能力和 SLO 一样,得靠体系,不靠一次冲刺。
本文是 SRE 实战系列第 16 篇。至此,这个系列已经覆盖四条主线:可靠性的度量与承诺(3、4、5、10 篇)、告警与响应(6、8、13 篇)、可观测性(7、12、14 篇)、工程效能与组织(9、11、15 篇)。本篇补上第五条:成本——它是 SRE 唯一一个能直接换算成钱的职责维度。
下一篇预告:SRE 成熟度模型——你的团队在第几级,以及每一级怎么往上跳。